﻿/*
 *  Copyright (c) 2009 Alessandro Pilotti
 *
 *  Permission is hereby granted, free of charge, to any person
 *  obtaining a copy of this software and associated documentation
 *  files (the "Software"), to deal in the Software without
 *  restriction, including without limitation the rights to use,
 *  copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following
 *  conditions:
 *  
 *  The above copyright notice and this permission notice shall be
 *  included in all copies or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *  OTHER DEALINGS IN THE SOFTWARE.
 */

using System;
using System.IO;
using System.Reflection;
using System.Security;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;

namespace AlexPilotti.KeyStorageManager
{
    public class DPAPIXmlKeyStorageProviderManager : IKeyStorageProviderManager
    {
        #region IKeyStorageProviderManager Members

        public bool IsAvailable
        {
            get
            {
                return (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 5) ||
                       (Environment.OSVersion.Platform == PlatformID.WinCE && Environment.OSVersion.Version.Major >= 4);
            }
        }

        public IKeyStorageProvider GetProviderInstance()
        {
            return new DPAPIXmlKeyStorageProvider();
        }

        #endregion
    }

    public sealed class DPAPIXmlKeyStorageProvider : DPAPIKeyStorageProvider
    {
        #region Private Data

        const string keyStorageNs = "http://www.alessandropilotti.com/DPAPIKeyStorage";
        const string xsdResourceName = "AlexPilotti.KeyStorageManager.DPAPIKeyStorage.xsd";
        const string defaultXmlKeyStorageFileName = "KeyStorage.xml";

        string keyStorageXmlPath = null;

        #endregion

        #region Public Properties

        /// <summary>
        /// Path to the xml file path containing the stored keys. Default is "KeyStorage.xml" in the user's local application data folder.
        /// </summary>
        public string KeyStorageXmlPath
        {
            get
            {
                if (keyStorageXmlPath == null)
                    keyStorageXmlPath = GetDefaultXmlKeyStoragePath();
                
                return keyStorageXmlPath;             
            }
            set { keyStorageXmlPath = value; }
        }

        #endregion

        #region Constructors

        internal DPAPIXmlKeyStorageProvider()
        {
        }

        #endregion

        #region Private Methods

        private string GetDefaultXmlKeyStoragePath()
        {
            return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), defaultXmlKeyStorageFileName);
        }

        private XmlDocument LoadXml()
        {
            XmlDocument doc = new XmlDocument();

            if (File.Exists(KeyStorageXmlPath))
            {
                // The XSD file is embedded in the resources
                XmlSchema schema;
                using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream(xsdResourceName))
                    schema = XmlSchema.Read(s, null);

                XmlReaderSettings settings = new XmlReaderSettings();
                settings.Schemas.Add(schema);
                //settings.Schemas.Add(keyringNs, defaultKeyringXsdPath);
                settings.ValidationType = ValidationType.Schema;

                using (XmlReader reader = XmlReader.Create(KeyStorageXmlPath, settings))
                    doc.Load(reader);
            }
            else
            {
                XmlDeclaration dec = doc.CreateXmlDeclaration("1.0", "utf-8", null);
                doc.AppendChild(dec);
                XmlElement root = doc.CreateElement("keystorage", keyStorageNs);
                doc.AppendChild(root);
            }

            return doc;
        }

        private void SaveKeyStorageXmlFile(XmlDocument doc)
        {
            doc.Save(KeyStorageXmlPath);
        }

        private XPathNavigator FindNode(string name, XPathNavigator navigator)
        {
            // Using Linq would be nice, but we are stuck with .Net 2.0 for Mono compatibility
            XmlNamespaceManager manager = new XmlNamespaceManager(navigator.NameTable);
            manager.AddNamespace("ks", keyStorageNs);

            string xpath = "/ks:keystorage/ks:key[@name=\"" + SecurityElement.Escape(name) + "\"]";

            return navigator.SelectSingleNode(xpath, manager);
        }

        #endregion

        #region DPAPIKeyStorageProvider Overrides

        protected override byte[] LoadData(string name)
        {
            byte[] data = null;

            XmlDocument doc = LoadXml();
            XPathNavigator navigator = doc.CreateNavigator();
            XPathNavigator node = FindNode(name, navigator);
            if (node != null)
                data = (byte[])node.TypedValue;
            return data;
        }

        protected override void StoreData(string name, byte[] secret)
        {
            XmlDocument doc = LoadXml();
            XPathNavigator navigator = doc.CreateNavigator();
            XPathNavigator node = FindNode(name, navigator);

            if (node != null)
                node.SetTypedValue(secret);
            else
            {
                navigator.MoveToFirstChild();
                using (XmlWriter wr = navigator.AppendChild())
                {
                    wr.WriteStartElement("key", keyStorageNs);
                    wr.WriteAttributeString("name", name);
                    wr.WriteBase64(secret, 0, secret.Length);
                    wr.WriteEndElement();
                }
            }

            SaveKeyStorageXmlFile(doc);
        }

        protected override bool DeleteData(string name)
        {
            XmlDocument doc = LoadXml();
            XPathNavigator navigator = doc.CreateNavigator();
            XPathNavigator node = FindNode(name, navigator);

            if (node != null)
            {
                node.DeleteSelf();
                SaveKeyStorageXmlFile(doc);
            }

            return node != null;
        }

        #endregion
    }
}
